package tool.classloader;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;

/**rappresenta un classLoader in grado di reperire file .class da un sottoalbero del file System.
 * Affinche' il Loader possa scolgere al meglio la sua funzione, e' necessario che:
 * <ul>
 *  <li>non ci siano link nel sotto file system (sia hardware che software)</li>
 *  <li>il soot file system non sia troppo grande</li>
 * </ul>
 * <p>il sottoalbero filesystem ha come sua unica radice il file (che deve rappresentare una directory)
 * {@link #directory}: la classe infatti non esegue alcun controllo sul fatot che tale file rappresenti
 * o meno una directory.
 * <p>La classe, quindi, rappresenta l'oggetto che si preoccupa di caricare le classi esterne dal classpath locale.
 * La classe necessita di una cartella di radice con cui operare: in questa cartella di radice poi esiste
 * un albero gerarchico di cartelle che rappresenta i vari package in cui i class file sono contenuti.
 * <p> per esempio ipotizziamo che vogliamo caricare la classe "Bee.class" appartenente al package
 * renderer.character.player: allora nella cartella {@link #directory} ci sara' una cartella "renderer"
 * contenente una cartella "character" contenente una cartella "player" con all'interno il file class "Bee"
 * 
 * @author Koldar
 * @version 1.2
 */
public abstract class FileSystemClassLoader extends ClassLoader{

	//FIXME è sicuramente possibile alleggerire FORTEMENTE la classe usando le primitive di CLassLoader come "findResource("nome classe") [per poi convertire l'URL in binary name magari] o "getPackage")
	
	//FIXME migliorare la classe: non è possibile che estensioni di FileSystemClassLoader necessiino di riscrivere l'algoritmo di scanDiorectory da capo!!!
	
	/**filtro usato per ricavare i singoli file class nell'albero gerarchico*/
	protected static final FileSystemFilter CLASSFILTER=new FileSystemFilter(false,"only class accepted","class");
	/**filtro usato per ricavare le singole sottocartelle all'interno dell'albero gerarchico al di sotto di {@link #directory}*/
	protected static final FileSystemFilter DIRECTORYFILTER=new FileSystemFilter(true,"only directory accepted");
	/**indica la directory scelta per caricare i file class*/
	protected File directory;
	/**rappresenta una mappa che possiede come chiavi (K) i nomi delle classi. I dati (V) sono invece i nomi binari di tali classi*/
	protected HashMap<String,String> classfiles;
	
	public FileSystemClassLoader(ClassLoader parent,File directory){
		super(parent);
		this.directory=directory;
		this.classfiles=new HashMap<String,String>();
	}
	
	/**costruisce un nuovo classLoader vuoto, cioe' senza impostare il
	 * parametro fondamentale, la {@link #directory}. L'eventuale
	 * esecuzione del metodo {@link #scanDirectory()} potrebbe
	 * portare a funzionamento imprevisti
	 */
	public FileSystemClassLoader(){
		super();
		this.classfiles=new HashMap<String,String>();
	}

	/**
	 * @return the directory
	 */
	public File getDirectory() {
		return directory;
	}


	/**reimposta la {@link #directory} e resetta la mappa {@link #classfiles}
	 * 
	 * @param directory the directory to set
	 */
	public void setDirectory(File directory) {
		this.directory = directory;
		this.classfiles.clear();
	}


	/**
	 * @return the classfiles
	 */
	public HashMap<String, String> getClassfiles() {
		return classfiles;
	}


	/**costurisce un snooper di caricamento classe che setaccia l'albero gerarchico di file system
	 * partendo da {@link #directory} avente come class loader padre il getSystemClassLoader
	 * 
	 * @param directory indica la directory di base da cui il FileCLassLoader incomincer� a cercare
	 */
	public FileSystemClassLoader(File directory) {
		this(ClassLoader.getSystemClassLoader(),directory);
	}
	public FileSystemClassLoader(String directorypath){
		this(new File(directorypath));
	}
	/**costruisce un nuovo classLoader scannerizzando anche la {@link #directory}
	 * 
	 * @param directory indica la radice del file system da analizzare
	 * @param scan se TRUE indica il fatto che l'utente desidera scannerizzare la {@link #directory}
	 */
	public FileSystemClassLoader(File directory,boolean scan){
		this(directory);
		if (scan){
			this.scanDirectory();
		}
	}
	public FileSystemClassLoader(String directory,boolean scan){
		this(new File(directory),scan);
	}
	
	/**scannerizza l'intera directory {@link #directory} per ricercare tutti i file .class al suo interno; il
	 * metodo rileva anche i file .class che sono contenuti all'interno di directory contenute in {@link #directory}.
	 * Affinch� il metodo non degeneri in loop infiniti � consigliabile che il l'albero delle directory contenute
	 * in {@link #directory} non abbia link hardware o software ad altre cartelle. Inoltre � consigliabile
	 * che la directory {@link #directory} non sia grande (questo per evitare attese prolungate).
	 * <p>{@link #scanDirectory()} popola il vettore {@link #classfiles} in modo che questo classloader possa
	 * caricare in modo pi� veloce i file esterni. 
	 * 
	 */
	public void scanDirectory(){
		this.classfiles=this.scanDirectory(directory,"");
	}
	
	/**carica la classe con il determinato nome <tt>classname</tt>. in caso non sia stata caricata, lancia
	 * l'eccezione ClassNotFoundException
	 * 
	 * @param classname
	 * @return
	 * @throws ClassNotFoundException
	 */
	public Class<?> loadNameClass(String classname) throws ClassNotFoundException{
		
		/*DEBUG
		for (String k:this.classfiles.keySet()){
			System.out.println(k+" --> "+this.classfiles.get(k));
		}*/
		if (this.classfiles.containsKey(classname)){
			return this.loadFromURL(this.classfiles.get(classname));
		}else{
			throw new ClassNotFoundException();
		}
	}
	
	public Class<?> findClass(String binaryname) throws ClassNotFoundException{
		if (this.classfiles.values().contains(binaryname)){
			return this.loadFromURL(binaryname);
		}else{
			throw new ClassNotFoundException();
		}
	}
	
	/**carica la classe specificata da classname che � contenuta nella serie specificata da packagename.
	 * <p>ESEMPIO: immaginiamo di voler caricare la classe Bee all'interno del pacchetto players contenuto nel
	 * pacchetto characters contenuto nel pacchetto game. allora i parametri della classe saranno:
	 * 
	 * <p><tt>loadClass("Bee","game","characters","players");</tt>
	 * 
	 * @param classname indica il nome della classe (senza estensione .class!) da caricare
	 * @param packagename indica la lista di package che contiene la classe
	 * @return un'istanza Class della particolare classe o NULL se avviene qualche errore
	 * @throws ClassNotFoundException in caso in cui la classe non venga trovata
	 */
	public Class<?> findClass(String classname,String ...packagename) throws ClassNotFoundException{
		String packagepath="";
		for (int i=0;i<packagename.length;i++){
			packagepath=packagepath+packagename[i]+".";
		}
		return this.loadFromURL(packagepath+classname);
	}
	
	
	
	/**dato un binaryname corretto sintatticamente, carica la classe utilizzando come ClassLoader
	 * un URLClassLoader
	 * 
	 * @param binaryname il binaryname da caricare (esprimente la classe da caricare)
	 * @return un'istanza di Class relativa alla particolare classe, NULL se avviene qualche tipo di errore
	 * @throws ClassNotFoundException in caso non si riesca a trovare la classe specificata da binaryname
	 * @throws NoClassDefFoundException in caso in cui la classe da caricar erichieda altre classi non incluse nel classpath
	 */
	protected Class<?> loadFromURL(String binaryname) throws ClassNotFoundException,NoClassDefFoundError{
		//TODO cercare un metodo migliore... mi sfugge qualcosa qui dentro! provare a modificare il classpath!
		URL dirURL;
		try {
			dirURL = new URL("file:/" + this.directory.getAbsolutePath() + "/");
			URLClassLoader cl = new URLClassLoader(
					new URL[] {dirURL},
					getClass().getClassLoader());
			return cl.loadClass(binaryname);
		} catch (MalformedURLException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * 
	 * @param f il file da cui reperire il nome
	 * @return il nome del file <strong>senza</strong> estensione
	 */
	protected static String getFileName(File f){
		return f.getName().substring(0,f.getName().lastIndexOf("."));
	}
	/**
	 * 
	 * @param f la stringa rappresentante il nome di un file da cui reperire il singolo nome 
	 * @return il nome del file <strong>senza</strong> estensione
	 */
	protected static String getFileName(String f){
		return f.substring(0,f.lastIndexOf("."));
	}
	
	
	/**data una directory, ritorna tutti i binaryname dei file .class nella directory. Affinch� il metodo
	 * funzioni in modo corretto e' <strong>consigliabile</strong> che:
	 * <ul>
	 *  <li>il file system in <tt>rootdir</tt> non contenga link (sia hardware che software)</li> 
	 *  <li>il file system non sia troppo grande in quanto questo potrebbe far diminuire in modo folle le prestazioni</li>
	 * <ul>
	 * @param rootdir la directory che bisogna settacciare per trovare i file .class
	 * @param relativepath indica il percorso eseguito partendo da {@link #directory} per arrivare alla rootdir (in binaryname, cio� al posto delle "/" i ".")
	 * @return una lista di binaryname delle varie classi trovate
	 */
	protected abstract HashMap<String,String> scanDirectory(File rootdir,String relativepath);
	
}
